/*
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 * 
 */
package com.aelitis.azureus.plugins.startstoprules.defaultplugin;

import java.util.Iterator;
import java.util.List;

import org.gudy.azureus2.core3.config.COConfigurationListener;
import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.download.DownloadManager;
import org.gudy.azureus2.core3.download.DownloadManagerState;
import org.gudy.azureus2.core3.download.DownloadManagerStateAttributeListener;
import org.gudy.azureus2.core3.util.AEMonitor;
import org.gudy.azureus2.core3.util.DisplayFormatters;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.core3.util.TimeFormatter;
import org.gudy.azureus2.plugins.PluginConfig;
import org.gudy.azureus2.plugins.download.Download;
import org.gudy.azureus2.plugins.download.DownloadScrapeResult;
import org.gudy.azureus2.plugins.download.DownloadStats;
import org.gudy.azureus2.plugins.logging.LoggerChannel;
import org.gudy.azureus2.pluginsimpl.local.PluginCoreUtils;

/**
 * @author TuxPaper
 * @created Dec 13, 2005
 * 
 */
public class DefaultRankCalculator implements DownloadManagerStateAttributeListener, Comparable {
    /** All of the First Priority rules must match */
    public static final int FIRSTPRIORITY_ALL = 0;

    /** Any of the First Priority rules must match */
    public static final int FIRSTPRIORITY_ANY = 1;

    /**
     * Force torrent to be "Actively Seeding/Downloading" for this many ms upon start of torrent.
     */
    private static final int FORCE_ACTIVE_FOR = 30000;

    /**
     * Wait XX ms before really changing activity (DL or CDing) state when state changes via speed change
     */
    private static final int ACTIVE_CHANGE_WAIT = 10000;

    /** Maximium ranking that a torrent can get using the SPRATIO ranking type */
    private static int SPRATIO_BASE_LIMIT = 99999;

    /**
     * Amount to shift over the rank of the SEEDONLY ranking type, to make room in case the user has fallback to SPRATIO set.
     */
    private static int SEEDONLY_SHIFT = SPRATIO_BASE_LIMIT + 1;

    /**
     * For loading config settings
     */
    private static COConfigurationListener configListener = null;

    //
    // Seeding Rank (SR) Limits and Values

    /** Rank that complete starts at (and incomplete ends at + 1) */
    public static final int SR_COMPLETE_STARTS_AT = 1000000000; // billion

    /** Maximimum ranking for time queue mode. 1 unit is a second */
    public static final int SR_TIMED_QUEUED_ENDS_AT = 999999; // 11.57 days

    /** Ranks below this value are for torrents to be ignored (moved to bottom & queued) */
    public static final int SR_IGNORED_LESS_THAN = -1;

    /** Seeding Rank value when download is marked as not queued */
    public static final int SR_NOTQUEUED = -2;

    /** Seeding Rank value when download is marked as S:P Ratio Met for FP */
    public static final int SR_FP_SPRATIOMET = -3;

    /** Seeding Rank value when download is marked as P:1S Ratio Met */
    public static final int SR_RATIOMET = -4;

    /** Seeding Rank value when download is marked as # Seeds Met */
    public static final int SR_NUMSEEDSMET = -5;

    /** Seeding Rank value when download is marked as 0 Peers and FP */
    public static final int SR_FP0PEERS = -6;

    /** Seeding Rank value when download is marked as 0 Peers */
    public static final int SR_0PEERS = -7;

    /** Seeding Rank value when download is marked as Share Ratio Met */
    public static final int SR_SHARERATIOMET = -8;

    public static final String[] SR_NEGATIVE_DEBUG = { "?", "Not Qd", "FP SPRatioMet", "Ratio Met", "# CDs Met", "FP 0 Peers", "0 Peers",
            "Share Ratio Met" };

    private static final long STALE_REFRESH_INTERVAL = 1000 * 60;

    //
    // Static config values

    /** Ranking System to use */
    protected static int iRankType = -1;

    /** Min # of Peers needed before boosting the rank of downloads with no seeds */
    private static int minPeersToBoostNoSeeds;

    /** Min Speed needed to count a incomplete download as being actively downloading */
    private static int minSpeedForActiveDL;

    /** Min speed needed to count a complete download as being actively seeding */
    private static int minSpeedForActiveSeeding;

    // Ignore torrent if seed count is at least..
    private static int iIgnoreSeedCount;

    // Ignore even when First Priority
    private static boolean bIgnore0Peers;

    private static int iIgnoreShareRatio;

    private static int iIgnoreShareRatio_SeedStart;

    private static int iIgnoreRatioPeers;

    private static int iIgnoreRatioPeers_SeedStart;

    private static int iRankTypeSeedFallback;

    private static boolean bPreferLargerSwarms;

    private static int minQueueingShareRatio;

    // Ignore First Priority
    private static int iFirstPriorityIgnoreSPRatio;

    private static boolean bFirstPriorityIgnore0Peer;

    private static int iFirstPriorityType;

    private static int iFirstPrioritySeedingMinutes;

    private static int iFirstPriorityActiveMinutes;

    private static int iFirstPriorityIgnoreIdleHours;

    private static long minTimeAlive;

    private static boolean bAutoStart0Peers;

    //
    // Class variables

    protected final Download dl;

    private boolean bActivelyDownloading;

    private long lDLActivelyChangedOn;

    private boolean bActivelySeeding;

    private long lCDActivelyChangedOn;

    private long staleCDSince;

    private long staleCDOffset;

    private long lastStaleCDRefresh;

    private boolean bIsFirstPriority;

    private int dlSpecificMinShareRatio;
    private int dlSpecificMaxShareRatio;

    /** Public for tooltip to access it */
    public String sExplainFP = "";

    /** Public for tooltip to access it */
    public String sExplainSR = "";

    /** Public for tooltip to access it */
    public String sTrace = "";

    private AEMonitor downloadData_this_mon = new AEMonitor("StartStopRules:downloadData");

    private final StartStopRulesDefaultPlugin rules;

    // state-caches for sorting

    int lastModifiedScrapeResultPeers = 0;
    int lastModifiedScrapeResultSeeds = 0;
    int lastModifiedShareRatio = 0;
    // modified by a listener in StartStopRulesDefaultPlugin
    boolean lastScrapeResultOk = false;

    /**
     * Default Initializer
     * 
     * @param _rules
     * @param _dl
     */
    public DefaultRankCalculator(StartStopRulesDefaultPlugin _rules, Download _dl) {
        rules = _rules;
        dl = _dl;

        DownloadManager core_dm = PluginCoreUtils.unwrap(dl);

        DownloadManagerState dm_state = core_dm.getDownloadState();

        dlSpecificMinShareRatio = dm_state.getIntParameter(DownloadManagerState.PARAM_MIN_SHARE_RATIO);
        dlSpecificMaxShareRatio = dm_state.getIntParameter(DownloadManagerState.PARAM_MAX_SHARE_RATIO);

        dm_state.addListener(this, DownloadManagerState.AT_PARAMETERS, DownloadManagerStateAttributeListener.WRITTEN);

        try {
            downloadData_this_mon.enter();

            if (configListener == null) {

                configListener = new COConfigurationListener() {
                    public void configurationSaved() {
                        reloadConfigParams(rules.plugin_config);
                    }
                };

                COConfigurationManager.addListener(configListener);
                configListener.configurationSaved();
            }
        } finally {
            downloadData_this_mon.exit();
        }
    }

    public void attributeEventOccurred(DownloadManager download, String attribute, int event_type) {
        DownloadManager core_dm = PluginCoreUtils.unwrap(dl);

        DownloadManagerState dm_state = core_dm.getDownloadState();

        dlSpecificMinShareRatio = dm_state.getIntParameter(DownloadManagerState.PARAM_MIN_SHARE_RATIO);
        dlSpecificMaxShareRatio = dm_state.getIntParameter(DownloadManagerState.PARAM_MAX_SHARE_RATIO);
    }

    protected void destroy() {
        DownloadManager core_dm = PluginCoreUtils.unwrap(dl);

        DownloadManagerState dm_state = core_dm.getDownloadState();

        dm_state.removeListener(this, DownloadManagerState.AT_PARAMETERS, DownloadManagerStateAttributeListener.WRITTEN);
    }

    /**
     * Load config values into the static variables
     * 
     * @param cfg
     */
    public static void reloadConfigParams(PluginConfig cfg) {
        final String PREFIX = "StartStopManager_";

        iRankType = cfg.getUnsafeIntParameter(PREFIX + "iRankType");

        minPeersToBoostNoSeeds = cfg.getUnsafeIntParameter(PREFIX + "iMinPeersToBoostNoSeeds");
        minSpeedForActiveDL = cfg.getUnsafeIntParameter(PREFIX + "iMinSpeedForActiveDL");
        minSpeedForActiveSeeding = cfg.getUnsafeIntParameter(PREFIX + "iMinSpeedForActiveSeeding");

        iRankTypeSeedFallback = cfg.getUnsafeIntParameter(PREFIX + "iRankTypeSeedFallback");
        bPreferLargerSwarms = cfg.getUnsafeBooleanParameter(PREFIX + "bPreferLargerSwarms");
        minTimeAlive = cfg.getUnsafeIntParameter(PREFIX + "iMinSeedingTime") * 1000;
        bAutoStart0Peers = cfg.getUnsafeBooleanParameter(PREFIX + "bAutoStart0Peers");

        // Ignore torrent if seed count is at least..
        iIgnoreSeedCount = cfg.getUnsafeIntParameter(PREFIX + "iIgnoreSeedCount");
        bIgnore0Peers = cfg.getUnsafeBooleanParameter(PREFIX + "bIgnore0Peers");
        iIgnoreShareRatio = (int) (1000 * cfg.getFloatParameter("Stop Ratio"));
        iIgnoreShareRatio_SeedStart = cfg.getUnsafeIntParameter(PREFIX + "iIgnoreShareRatioSeedStart");
        iIgnoreRatioPeers = cfg.getIntParameter("Stop Peers Ratio", 0);
        iIgnoreRatioPeers_SeedStart = cfg.getUnsafeIntParameter(PREFIX + "iIgnoreRatioPeersSeedStart", 0);

        minQueueingShareRatio = cfg.getUnsafeIntParameter(PREFIX + "iFirstPriority_ShareRatio");
        iFirstPriorityType = cfg.getUnsafeIntParameter(PREFIX + "iFirstPriority_Type");
        iFirstPrioritySeedingMinutes = cfg.getUnsafeIntParameter(PREFIX + "iFirstPriority_SeedingMinutes");
        iFirstPriorityActiveMinutes = cfg.getUnsafeIntParameter(PREFIX + "iFirstPriority_DLMinutes");
        // Ignore FP
        iFirstPriorityIgnoreSPRatio = cfg.getUnsafeIntParameter(PREFIX + "iFirstPriority_ignoreSPRatio");
        bFirstPriorityIgnore0Peer = cfg.getUnsafeBooleanParameter(PREFIX + "bFirstPriority_ignore0Peer");
        iFirstPriorityIgnoreIdleHours = cfg.getUnsafeIntParameter(PREFIX + "iFirstPriority_ignoreIdleHours");
    }

    /**
     * Sort first by SeedingRank Descending, then by Position Ascending.
     */
    public int compareTo(Object obj) {
        if (!(obj instanceof DefaultRankCalculator)) {
            return -1;
        }

        DefaultRankCalculator dlData = (DefaultRankCalculator) obj;

        // Test FP. FP goes to top
        if (dlData.bIsFirstPriority && !bIsFirstPriority)
            return 1;
        if (!dlData.bIsFirstPriority && bIsFirstPriority)
            return -1;

        // Test Completeness. Complete go to bottom
        boolean aIsComplete = dlData.dl.isComplete();
        boolean bIsComplete = dl.isComplete();
        if (aIsComplete && !bIsComplete)
            return -1;
        if (!aIsComplete && bIsComplete)
            return 1;

        if (iRankType == StartStopRulesDefaultPlugin.RANK_NONE) {
            return dl.getPosition() - dlData.dl.getPosition();
        }

        // Check Rank. Large to top
        int value = dlData.dl.getSeedingRank() - dl.getSeedingRank();
        if (value != 0)
            return value;

        if (iRankType != StartStopRulesDefaultPlugin.RANK_TIMED) {
            // Test Large/Small Swarm pref
            int numPeersThem = dlData.lastModifiedScrapeResultPeers;
            int numPeersUs = lastModifiedScrapeResultPeers;
            if (bPreferLargerSwarms)
                value = numPeersThem - numPeersUs;
            else
                value = numPeersUs - numPeersThem;
            if (value != 0)
                return value;

            // Test Share Ratio
            value = lastModifiedShareRatio - dlData.lastModifiedShareRatio;
            if (value != 0)
                return value;
        }

        // Test Position
        return dl.getPosition() - dlData.dl.getPosition();
    }

    public Download getDownloadObject() {
        return dl;
    }

    public boolean isForceActive() {
        DownloadStats stats = dl.getStats();
        return SystemTime.getCurrentTime() - stats.getTimeStarted() <= FORCE_ACTIVE_FOR;
    }

    public boolean isQueued() {
        return (dl.getState() == Download.ST_QUEUED);
    }

    /**
     * Retrieves whether the torrent is "actively" downloading
     * 
     * @return true: actively downloading
     */
    public boolean getActivelyDownloading() {
        boolean bIsActive = false;
        DownloadStats stats = dl.getStats();
        int state = dl.getState();

        // In order to be active,
        // - Must be downloading (and thus incomplete)
        // - Must be above speed threshold, or started less than 30s ago
        if (state != Download.ST_DOWNLOADING) {
            bIsActive = false;
        } else if (SystemTime.getCurrentTime() - stats.getTimeStarted() <= FORCE_ACTIVE_FOR) {
            bIsActive = true;
        } else {
            // activity based on DL Average
            bIsActive = (stats.getDownloadAverage() >= minSpeedForActiveDL);

            if (bActivelyDownloading != bIsActive) {
                long now = SystemTime.getCurrentTime();
                // Change
                if (lDLActivelyChangedOn == -1) {
                    // Start Timer
                    lDLActivelyChangedOn = now;
                    bIsActive = !bIsActive;
                } else if (now - lDLActivelyChangedOn < ACTIVE_CHANGE_WAIT) {
                    // Continue as old state until timer finishes
                    bIsActive = !bIsActive;
                }
            } else {
                // no change, reset timer
                lDLActivelyChangedOn = -1;
            }
        }

        if (bActivelyDownloading != bIsActive) {
            bActivelyDownloading = bIsActive;
            if (rules != null) {
                rules.requestProcessCycle(null);
                if (rules.bDebugLog)
                    rules.log.log(dl.getTorrent(), LoggerChannel.LT_INFORMATION, "somethingChanged: ActivelyDownloading changed");
            }
        }
        return bActivelyDownloading;
    }

    /**
     * Retrieves whether the torrent is "actively" seeding
     * 
     * @return true: actively seeding
     */
    public boolean getActivelySeeding() {
        boolean bIsActive = false;
        DownloadStats stats = dl.getStats();
        int state = dl.getState();
        // Timed torrents don't use a speed threshold, since they are based on time!
        // However, First Priorities need to be checked for activity so that
        // timed ones can start when FPs are below threshold. Ditto for 0 Peers
        // when bAutoStart0Peers
        if (iRankType == StartStopRulesDefaultPlugin.RANK_TIMED && !isFirstPriority()
                && !(bAutoStart0Peers && rules.calcPeersNoUs(dl, dl.getAggregatedScrapeResult()) == 0 && lastScrapeResultOk)) {
            bIsActive = (state == Download.ST_SEEDING);

        } else if (state != Download.ST_SEEDING || (bAutoStart0Peers && rules.calcPeersNoUs(dl, dl.getAggregatedScrapeResult()) == 0)) {
            // Not active if we aren't seeding
            // Not active if we are AutoStarting 0 Peers, and peer count == 0
            bIsActive = false;
            staleCDSince = -1;
        } else if (SystemTime.getCurrentTime() - stats.getTimeStarted() <= FORCE_ACTIVE_FOR) {
            bIsActive = true;
            staleCDSince = -1;
        } else {
            bIsActive = (stats.getUploadAverage() >= minSpeedForActiveSeeding);

            if (bActivelySeeding != bIsActive) {
                long now = SystemTime.getCurrentTime();
                // Change
                if (lCDActivelyChangedOn < 0) {
                    // Start Timer
                    lCDActivelyChangedOn = now;
                    bIsActive = !bIsActive;
                } else if (now - lCDActivelyChangedOn < ACTIVE_CHANGE_WAIT) {
                    // Continue as old state until timer finishes
                    bIsActive = !bIsActive;
                }

                if (bActivelySeeding != bIsActive) {
                    if (bIsActive) {
                        staleCDSince = -1;
                        staleCDOffset = 0;
                    } else {
                        staleCDSince = System.currentTimeMillis();
                    }
                }

            } else {
                // no change, reset timer
                lCDActivelyChangedOn = -1;
            }
        }

        if (bActivelySeeding != bIsActive) {
            bActivelySeeding = bIsActive;

            if (rules != null) {
                rules.requestProcessCycle(null);
                if (rules.bDebugLog)
                    rules.log.log(dl.getTorrent(), LoggerChannel.LT_INFORMATION, "somethingChanged: ActivelySeeding changed");
            }
        }
        return bActivelySeeding;
    }

    /**
     * Assign Seeding Rank based on RankType
     * 
     * @return New Seeding Rank Value
     */
    public int recalcSeedingRank() {
        try {
            downloadData_this_mon.enter();

            int oldSR = dl.getSeedingRank();

            int newSR = _recalcSeedingRankSupport(oldSR);

            if (newSR != oldSR) {

                dl.setSeedingRank(newSR);
            }
            return (newSR);

        } finally {

            downloadData_this_mon.exit();
        }
    }

    private int _recalcSeedingRankSupport(int oldSR) {

        sExplainSR = "";

        DownloadStats stats = dl.getStats();

        int newSR = 0;

        // make undownloaded sort to top so they can start first.
        if (!dl.isComplete()) {
            newSR = SR_COMPLETE_STARTS_AT + (10000 - dl.getPosition());
            // make sure we capture FP being turned off when torrent does from
            // complete to incomplete
            isFirstPriority();
            if (rules.bDebugLog) {
                sExplainSR += "  not complete. SetSR " + newSR + "\n";
            }
            return newSR;
        }

        // here we are seeding

        lastModifiedShareRatio = stats.getShareRatio();
        DownloadScrapeResult sr = dl.getAggregatedScrapeResult();
        lastModifiedScrapeResultPeers = rules.calcPeersNoUs(dl, sr);
        lastModifiedScrapeResultSeeds = rules.calcSeedsNoUs(dl, sr);

        boolean bScrapeResultsOk =
                (lastModifiedScrapeResultPeers > 0 || lastModifiedScrapeResultSeeds > 0 || lastScrapeResultOk)
                        && (lastModifiedScrapeResultPeers >= 0 && lastModifiedScrapeResultSeeds >= 0);

        if (!isFirstPriority()) {
            // Check Ignore Rules
            // never apply ignore rules to First Priority Matches
            // (we don't want leechers circumventing the 0.5 rule)

            // 0 means unlimited
            int activeMaxSR = dlSpecificMaxShareRatio;
            if (activeMaxSR <= 0) {
                activeMaxSR = iIgnoreShareRatio;
            }
            if (activeMaxSR != 0 && lastModifiedShareRatio >= activeMaxSR
                    && (lastModifiedScrapeResultSeeds >= iIgnoreShareRatio_SeedStart || !bScrapeResultsOk) && lastModifiedShareRatio != -1) {

                if (rules.bDebugLog)
                    sExplainSR += "  shareratio met: shareRatio(" + lastModifiedShareRatio + ") >= " + activeMaxSR + "\n";

                return SR_SHARERATIOMET;
            } else if (rules.bDebugLog && activeMaxSR != 0 && lastModifiedShareRatio >= activeMaxSR) {
                sExplainSR += "  shareratio NOT met: ";
                if (lastModifiedScrapeResultSeeds >= iIgnoreShareRatio_SeedStart)
                    sExplainSR += lastModifiedScrapeResultSeeds + " below seed threshold of " + iIgnoreShareRatio_SeedStart;
                sExplainSR += "\n";
            }

            if (lastModifiedScrapeResultPeers == 0 && bScrapeResultsOk) {
                // If both bIgnore0Peers and bFirstPriorityIgnore0Peer are on,
                // we won't know which one it is at this point.
                // We have to use the normal SR_0PEERS in case it isn't FP
                if (bIgnore0Peers) {
                    if (rules.bDebugLog)
                        sExplainSR += "  Ignore 0 Peers criteria met\n";

                    return SR_0PEERS;
                }

                // if (bFirstPriorityIgnore0Peer) {
                // if (rules.bDebugLog)
                // sExplainSR += "  Ignore 0 Peers criteria for FP met\n";
                //
                // return SR_FP0PEERS;
                // }
            } else if (rules.bDebugLog && lastModifiedScrapeResultPeers == 0) {
                sExplainSR += "  0 Peer Ignore rule NOT applied: Scrape invalid\n";
            }

            // if (numPeers != 0 && iFirstPriorityIgnoreSPRatio != 0
            // && numSeeds / numPeers >= iFirstPriorityIgnoreSPRatio) {
            // if (rules.bDebugLog)
            // sExplainSR += "  Ignore rule for S:P Ratio for FP met.  Current: ("
            // + (numSeeds / numPeers)
            // + ") >= Threshold("
            // + iFirstPriorityIgnoreSPRatio + ")\n";
            //
            // return SR_FP_SPRATIOMET;
            // }

            // 0 means disabled
            if ((iIgnoreSeedCount != 0) && (lastModifiedScrapeResultSeeds >= iIgnoreSeedCount)) {
                if (rules.bDebugLog)
                    sExplainSR +=
                            "  SeedCount Ignore rule met.  numSeeds(" + lastModifiedScrapeResultSeeds + " >= iIgnoreSeedCount(" + iIgnoreSeedCount
                                    + ")\n";

                return SR_NUMSEEDSMET;
            }

            // Ignore when P:S ratio met
            // (More Peers for each Seed than specified in Config)
            // 0 means never stop
            if (iIgnoreRatioPeers != 0 && lastModifiedScrapeResultSeeds != 0) {
                float ratio = (float) lastModifiedScrapeResultPeers / lastModifiedScrapeResultSeeds;
                if (ratio <= iIgnoreRatioPeers && lastModifiedScrapeResultSeeds >= iIgnoreRatioPeers_SeedStart) {

                    if (rules.bDebugLog)
                        sExplainSR += "  P:S Ignore rule met.  ratio(" + ratio + " <= threshold(" + iIgnoreRatioPeers_SeedStart + ")\n";

                    return SR_RATIOMET;
                }
            }
        }

        // Never do anything with rank type of none
        if (iRankType == StartStopRulesDefaultPlugin.RANK_NONE) {
            if (rules.bDebugLog)
                sExplainSR += "  Ranking Type set to none.. blanking seeding rank\n";

            // everythink ok!
            return newSR;
        }

        if (iRankType == StartStopRulesDefaultPlugin.RANK_TIMED) {
            if (bIsFirstPriority) {
                newSR += SR_TIMED_QUEUED_ENDS_AT + 1;
                return newSR;
            }

            int state = dl.getState();
            if (state == Download.ST_STOPPING || state == Download.ST_STOPPED || state == Download.ST_ERROR) {
                if (rules.bDebugLog)
                    sExplainSR += "  Download stopping, stopped or in error\n";
                return SR_NOTQUEUED;
            } else if (state == Download.ST_SEEDING || state == Download.ST_READY || state == Download.ST_WAITING || state == Download.ST_PREPARING) {
                // force sort to top
                long lMsElapsed = 0;
                if (state == Download.ST_SEEDING && !dl.isForceStart())
                    lMsElapsed = (SystemTime.getCurrentTime() - stats.getTimeStartedSeeding());

                if (lMsElapsed >= minTimeAlive) {
                    newSR = 1;
                    if (oldSR > SR_TIMED_QUEUED_ENDS_AT) {
                        rules.requestProcessCycle(null);
                        if (rules.bDebugLog)
                            rules.log.log(dl.getTorrent(), LoggerChannel.LT_INFORMATION, "somethingChanged: TimeUp");
                    }
                } else {
                    newSR = SR_TIMED_QUEUED_ENDS_AT + 1 + (int) (lMsElapsed / 1000);
                    if (oldSR <= SR_TIMED_QUEUED_ENDS_AT) {
                        rules.requestProcessCycle(null);
                        if (rules.bDebugLog)
                            rules.log.log(dl.getTorrent(), LoggerChannel.LT_INFORMATION, "somethingChanged: strange timer change");
                    }
                }
                return newSR;
            } else {
                if (oldSR <= 0) {
                    newSR = SR_TIMED_QUEUED_ENDS_AT - dl.getPosition();
                    rules.requestProcessCycle(null);
                    if (rules.bDebugLog)
                        rules.log.log(dl.getTorrent(), LoggerChannel.LT_INFORMATION, "somethingChanged: NotIgnored");
                }
                return newSR;
            }
        }

        /**
         * Add to SeedingRank based on Rank Type
         */

        // SeedCount and SPRatio require Scrape Results..
        if (bScrapeResultsOk) {
            if (iRankType == StartStopRulesDefaultPlugin.RANK_PEERCOUNT) {
                if (lastModifiedScrapeResultPeers > lastModifiedScrapeResultSeeds * 10)
                    newSR = 100 * lastModifiedScrapeResultPeers * 10;
                else
                    newSR =
                            (int) ((long) 100 * lastModifiedScrapeResultPeers * lastModifiedScrapeResultPeers / (lastModifiedScrapeResultSeeds + 1));
            } else if ((iRankType == StartStopRulesDefaultPlugin.RANK_SEEDCOUNT)
                    && (iRankTypeSeedFallback == 0 || iRankTypeSeedFallback > lastModifiedScrapeResultSeeds)) {
                if (lastModifiedScrapeResultSeeds < 10000)
                    newSR = 10000 - lastModifiedScrapeResultSeeds;
                else
                    newSR = 1;
                // shift over to make way for fallback
                newSR *= SEEDONLY_SHIFT;

            } else { // iRankType == RANK_SPRATIO or we are falling back
                if (lastModifiedScrapeResultPeers != 0) {
                    if (lastModifiedScrapeResultSeeds == 0) {
                        if (lastModifiedScrapeResultPeers >= minPeersToBoostNoSeeds)
                            newSR += SPRATIO_BASE_LIMIT;
                    } else { // numSeeds != 0 && numPeers != 0
                        float x = (float) lastModifiedScrapeResultSeeds / lastModifiedScrapeResultPeers;
                        newSR += SPRATIO_BASE_LIMIT / ((x + 1) * (x + 1));
                    }
                }
            }
        } else {
            if (rules.bDebugLog)
                sExplainSR += "  Can't calculate SR, no scrape results\n";
        }

        if (staleCDOffset > 0) {
            // every 10 minutes of not being active, subtract one SR
            if (newSR > staleCDOffset) {
                newSR -= staleCDOffset;
                sExplainSR += "  subtracted " + staleCDOffset + " due to non-activeness\n";
            } else {
                staleCDOffset = 0;
            }
        }

        if (newSR < 0)
            newSR = 1;

        return newSR;
    }

    /**
     * Does the torrent match First Priority criteria?
     * 
     * @return FP State
     */
    public boolean isFirstPriority() {
        boolean bFP = pisFirstPriority();

        if (bIsFirstPriority != bFP) {
            bIsFirstPriority = bFP;
            rules.requestProcessCycle(null);
            if (rules.bDebugLog)
                rules.log.log(dl.getTorrent(), LoggerChannel.LT_INFORMATION, "somethingChanged: FP changed");
        }
        return bIsFirstPriority;
    }

    private boolean pisFirstPriority() {
        if (rules.bDebugLog)
            sExplainFP = "FP if " + (iFirstPriorityType == FIRSTPRIORITY_ALL ? "all" : "any") + " criteria match:\n";

        if (!dl.isPersistent()) {
            if (rules.bDebugLog)
                sExplainFP += "Not FP: Download not persistent\n";
            return false;
        }

        int state = dl.getState();
        if (state == Download.ST_ERROR || state == Download.ST_STOPPED) {
            if (rules.bDebugLog)
                sExplainFP += "Not FP: Download is ERROR or STOPPED\n";
            return false;
        }

        // FP only applies to completed
        if (!dl.isComplete()) {
            if (rules.bDebugLog)
                sExplainFP += "Not FP: Download not complete\n";
            return false;
        }

        List listeners = rules.getFPListeners();
        StringBuffer fp_listener_debug = null;
        if (!listeners.isEmpty()) {
            if (rules.bDebugLog)
                fp_listener_debug = new StringBuffer();
            for (Iterator iter = listeners.iterator(); iter.hasNext();) {
                StartStopRulesFPListener l = (StartStopRulesFPListener) iter.next();
                boolean result = l.isFirstPriority(dl, lastModifiedScrapeResultSeeds, lastModifiedScrapeResultPeers, fp_listener_debug);
                if (fp_listener_debug != null && fp_listener_debug.length() > 0) {
                    char last_ch = fp_listener_debug.charAt(fp_listener_debug.length() - 1);
                    if (last_ch != '\n')
                        fp_listener_debug.append('\n');
                    sExplainFP += fp_listener_debug;
                    fp_listener_debug.setLength(0);
                }
                if (result) {
                    return true;
                }
            }
        }

        // FP doesn't apply when S:P >= set SPratio (SPratio = 0 means ignore)
        if (lastModifiedScrapeResultPeers > 0 && lastModifiedScrapeResultSeeds > 0
                && (lastModifiedScrapeResultSeeds / lastModifiedScrapeResultPeers) >= iFirstPriorityIgnoreSPRatio
                && iFirstPriorityIgnoreSPRatio != 0) {
            if (rules.bDebugLog)
                sExplainFP += "Not FP: S:P >= " + iFirstPriorityIgnoreSPRatio + ":1\n";
            return false;
        }

        // not FP if no peers //Nolar, 2105 - Gouss, 2203
        if (lastModifiedScrapeResultPeers == 0 && lastScrapeResultOk && bFirstPriorityIgnore0Peer) {
            if (rules.bDebugLog)
                sExplainFP += "Not FP: 0 peers\n";
            return false;
        }

        if (iFirstPriorityIgnoreIdleHours > 0) {
            long lastUploadSecs = dl.getStats().getSecondsSinceLastUpload();
            if (lastUploadSecs < 0) {
                lastUploadSecs = dl.getStats().getSecondsOnlySeeding();
            }
            if (lastUploadSecs > 60 * 60 * (long) iFirstPriorityIgnoreIdleHours) {
                if (rules.bDebugLog)
                    sExplainFP += "Not FP: " + lastUploadSecs + "s > " + iFirstPriorityIgnoreIdleHours + "h of no upload\n";
                return false;
            }
        }

        int shareRatio = dl.getStats().getShareRatio();

        int activeMinSR = dlSpecificMinShareRatio;
        if (activeMinSR <= 0) {
            activeMinSR = minQueueingShareRatio;
        }
        boolean bLastMatched = (shareRatio != -1) && (shareRatio < activeMinSR);

        if (rules.bDebugLog)
            sExplainFP += "  shareRatio(" + shareRatio + ") < " + activeMinSR + "=" + bLastMatched + "\n";

        if (!bLastMatched && iFirstPriorityType == FIRSTPRIORITY_ALL) {
            if (rules.bDebugLog)
                sExplainFP += "..Not FP.  Exit Early\n";
            return false;
        }
        if (bLastMatched && iFirstPriorityType == FIRSTPRIORITY_ANY) {
            if (rules.bDebugLog)
                sExplainFP += "..Is FP.  Exit Early\n";
            return true;
        }

        bLastMatched = (iFirstPrioritySeedingMinutes == 0);
        if (!bLastMatched) {
            long timeSeeding = dl.getStats().getSecondsOnlySeeding();
            if (timeSeeding >= 0) {
                bLastMatched = (timeSeeding < (iFirstPrioritySeedingMinutes * 60));
                if (rules.bDebugLog)
                    sExplainFP += "  SeedingTime(" + timeSeeding + ") < " + (iFirstPrioritySeedingMinutes * 60) + "=" + bLastMatched + "\n";
                if (!bLastMatched && iFirstPriorityType == FIRSTPRIORITY_ALL) {
                    if (rules.bDebugLog)
                        sExplainFP += "..Not FP.  Exit Early\n";
                    return false;
                }
                if (bLastMatched && iFirstPriorityType == FIRSTPRIORITY_ANY) {
                    if (rules.bDebugLog)
                        sExplainFP += "..Is FP.  Exit Early\n";
                    return true;
                }
            }
        } else if (rules.bDebugLog) {
            sExplainFP += "  Skipping Seeding Time check (user disabled)\n";
        }

        bLastMatched = (iFirstPriorityActiveMinutes == 0);
        if (!bLastMatched) {
            long timeActive = dl.getStats().getSecondsDownloading() + dl.getStats().getSecondsOnlySeeding();
            if (timeActive >= 0) {
                bLastMatched = (timeActive < (iFirstPriorityActiveMinutes * 60));
                if (rules.bDebugLog)
                    sExplainFP += "  ActiveTime(" + timeActive + ") < " + (iFirstPriorityActiveMinutes * 60) + "=" + bLastMatched + "\n";
                if (!bLastMatched && iFirstPriorityType == FIRSTPRIORITY_ALL) {
                    if (rules.bDebugLog)
                        sExplainFP += "..Not FP.  Exit Early\n";
                    return false;
                }
                if (bLastMatched && iFirstPriorityType == FIRSTPRIORITY_ANY) {
                    if (rules.bDebugLog)
                        sExplainFP += "..Is FP.  Exit Early\n";
                    return true;
                }
            }
        } else if (rules.bDebugLog) {
            sExplainFP += "  Skipping DL Time check (user disabled)\n";
        }

        if (iFirstPriorityType == FIRSTPRIORITY_ALL) {
            if (rules.bDebugLog)
                sExplainFP += "..Is FP\n";
            return true;
        }

        if (rules.bDebugLog)
            sExplainFP += "..Not FP\n";
        return false;
    }

    /**
     * 
     * @return last calculated FP state
     */
    public boolean getCachedIsFP() {
        return bIsFirstPriority;
    }

    private boolean dlr_test_active;
    private long dlr_test_start_time;
    private long dlr_test_bytes_start;
    private int dlr_test_average_bytes_per_sec = -1;

    public void setDLRInactive() {
        dlr_test_active = false;
    }

    public void setDLRActive(long time) {
        if (rules.bDebugLog) {
            rules.log.log(dl.getTorrent(), LoggerChannel.LT_INFORMATION, "download speed test starts");
        }

        dlr_test_active = true;

        dlr_test_start_time = time;

        dl.moveTo(1);

        dlr_test_bytes_start = dl.getStats().getDownloaded(true);
    }

    public void setDLRComplete(long time) {
        long dlr_test_bytes_end = dl.getStats().getDownloaded(true);

        long elapsed = time - dlr_test_start_time;

        if (elapsed >= 1000) {

            dlr_test_average_bytes_per_sec = (int) ((dlr_test_bytes_end - dlr_test_bytes_start) * 1000 / elapsed);

            if (rules.bDebugLog) {
                rules.log
                        .log(dl.getTorrent(), LoggerChannel.LT_INFORMATION, "download speed test ends - average=" + dlr_test_average_bytes_per_sec);
            }
        }

        dlr_test_active = false;
    }

    public long getDLRLastTestTime() {
        return (dlr_test_start_time);
    }

    public int getDLRLastTestSpeed() {
        return (dlr_test_average_bytes_per_sec);
    }

    public String getDLRTrace() {
        if (dlr_test_active) {

            return ("test in progress");

        } else if (dlr_test_start_time > 0) {

            if (dlr_test_average_bytes_per_sec >= 0) {

                return ("tested; " + TimeFormatter.format((SystemTime.getMonotonousTime() - dlr_test_start_time) / 1000) + " ago; " + "rate=" + DisplayFormatters
                        .formatByteCountToKiBEtcPerSec(dlr_test_average_bytes_per_sec));

            } else {

                return ("tested; " + TimeFormatter.format((SystemTime.getMonotonousTime() - dlr_test_start_time) / 1000) + " ago; " + "test did not complete");
            }
        } else {
            return ("");
        }
    }

    public String toString() {
        return String.valueOf(dl.getSeedingRank());
    }

    /**
     * Check Seeders for various changes not triggered by listeners
     * 
     * @return True: something changed
     */
    public boolean changeChecker() {
        if (getActivelySeeding()) {
            int shareRatio = dl.getStats().getShareRatio();
            int numSeeds = rules.calcSeedsNoUs(dl, dl.getAggregatedScrapeResult());

            int activeMaxSR = dlSpecificMaxShareRatio;
            if (activeMaxSR <= 0) {
                activeMaxSR = iIgnoreShareRatio;
            }
            if (activeMaxSR != 0 && shareRatio >= activeMaxSR && (numSeeds >= iIgnoreShareRatio_SeedStart || !lastScrapeResultOk)
                    && shareRatio != -1) {
                if (rules.bDebugLog) {
                    rules.log.log(dl.getTorrent(), LoggerChannel.LT_INFORMATION, "somethingChanged: shareRatio changeChecker");
                }
                return true;
            }
        }

        /*
         * READY downloads are usually waiting for a seeding torrent to stop (the seeding torrent probably is within the "Minimum Seeding Time"
         * setting) The rules may go through several cycles before a READY torrent is processed
         */
        if (dl.getState() == Download.ST_READY) {
            if (rules.bDebugLog)
                rules.log.log(dl.getTorrent(), LoggerChannel.LT_INFORMATION, "somethingChanged: Download is ready");
            return true;
        }

        if (staleCDSince > 0) {
            long now = SystemTime.getCurrentTime();
            if (now - lastStaleCDRefresh > STALE_REFRESH_INTERVAL) {
                staleCDOffset += (now - lastStaleCDRefresh) / STALE_REFRESH_INTERVAL;
                lastStaleCDRefresh = now;
                if (rules.bDebugLog) {
                    rules.log.log(dl.getTorrent(), LoggerChannel.LT_INFORMATION, "somethingChanged: staleCD changeChecker");
                }
                return true;
            }
        }

        return false;
    }
}
